/**
 * @file StarManager.cpp
 *
 * Odczyt i zapis informacji o gwiazdach - implementacja.
 *
 * @author Zbigniew Siciarz
 * @date 2009
 * @version 2.0.0
 * @since 2.0.0
 */

#include "StarManager.h"

#include "Exceptions.h"
#include "VisualObservation.h"
#include <QtCore>
#include <QtNetwork>
#include <QtSql>


namespace Dogz
{
    QStringList StarManager::s_starnames;

    /**
     * Tworzenie tabeli gwiazd w bazie danych.
     */
    void StarManager::createStarsTable()
    {
        QSqlDatabase db = QSqlDatabase::database();
        // zaczynamy transakcję
        db.transaction();

        // tworzenie struktury tabeli
        QSqlQuery query("CREATE TABLE IF NOT EXISTS stars ("
                        "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                        "name VARCHAR(30),"
                        "designation VARCHAR(10),"
                        "auid VARCHAR(12),"
                        "type VARCHAR(30),"
                        "max_magnitude DOUBLE,"
                        "min_magnitude DOUBLE,"
                        "period DOUBLE,"
                        "epoch DOUBLE"
                        ")");
        if (!query.isActive())
        {
            db.rollback();
            throw Dogz::DbException(query);
        }

        db.commit();
    }

    /**
     * Dodaje gwiazdę do bazy.
     *
     * @param star obiekt gwiazdy zmiennej
     * @return identyfikator dodanej gwiazdy, lub -1 w przypadku niepowodzenia
     */
    int StarManager::addStar(const Star& star)
    {
        QSqlDatabase db = QSqlDatabase::database();
        db.transaction();

        QSqlQuery query;
        query.prepare("INSERT INTO stars (name, designation, auid, "
                      "type, max_magnitude, min_magnitude, period, "
                      "epoch) VALUES(:name, :designation, :auid, :type, "
                      ":max_magnitude, :min_magnitude, :period, :epoch)");
        query.bindValue(":name", star.getName());
        query.bindValue(":designation", star.getDesignation());
        query.bindValue(":auid", star.getAuid());
        query.bindValue(":type", star.getType());
        query.bindValue(":max_magnitude", star.getMaxMagnitude());
        query.bindValue(":min_magnitude", star.getMinMagnitude());
        query.bindValue(":period", star.getPeriod());
        query.bindValue(":epoch", star.getEpoch());

        query.exec();
        if (!query.isActive())
        {
            db.rollback();
            throw Dogz::DbException(query);
        }

        db.commit();
        QVariant lastId = query.lastInsertId();
        return lastId.isValid() ? lastId.toInt() : -1;
    }

    /**
     * Pobiera gwiazdę o podanej nazwie z bazy danych.
     *
     * Metoda prywatna - zaleca się skorzystać z findStarByName().
     *
     * @param name nazwa gwiazdy (nie desygnacja ani AUID!)
     * @return obiekt gwiazdy, wypada go sprawdzać przez isValid()
     */
    Star StarManager::getStarFromDatabase(const QString& name)
    {
        Star result = Star::invalid();

        QSqlQuery query;
        query.prepare("SELECT * FROM stars WHERE name=:name");
        query.bindValue(":name", name);
        query.exec();
        if (!query.isActive())
            throw Dogz::DbException(query);

        if (query.next())
        {
            result.setName(query.value(Name).toString());
            result.setDesignation(query.value(Designation).toString());
            result.setAuid(query.value(Auid).toString());
            result.setType(query.value(Type).toString());
            result.setMagnitudeRange(query.value(MaxMagnitude).toDouble(),
                    query.value(MinMagnitude).toDouble());
            result.setPeriod(query.value(Period).toDouble());
            result.setEpoch(query.value(Epoch).toDouble());
        }

        return result;
    }

    /**
     * Wczytuje plik walidacji AAVSO.
     *
     * Na podstawie pliku walidacji tworzy listę nazw gwiazd znanych w AAVSO.
     *
     * @param filename ścieżka do pliku.
     */
    void StarManager::loadValidationFile(const QString& filename)
    {
        QFile file(filename);
        if (!file.exists())
            throw FileException(filename, QObject::tr("Brak pliku walidacji AAVSO: %1\n"
                    "Automatyczne podpowiadanie nazw gwiazd zostało wyłączone.").
                    arg(filename));

        if (file.open(QFile::ReadOnly))
        {
            QTextStream stream(&file);
            QString line;
            QStringList lineData;
            do
            {
                // wczytujemy linia po linii i wybieramy wartości z 2 kolumny
                line = stream.readLine();
                lineData = line.split(",");
                if (lineData.size() > 1)
                {
                    s_starnames.append(Star::normalizeStarName(
                            lineData[1].simplified()));
                }
            } while(!line.isNull());
        }
        else
        {
            throw FileException(filename, QObject::tr("Nie można odczytać pliku "
                    "walidacji AAVSO: %1\n"
                    "Automatyczne podpowiadanie nazw gwiazd zostało wyłączone.").
                    arg(filename));
        }
    }

    /**
     * Wyszukuje gwiazdę w lokalnej bazie lub w GCVS.
     *
     * @param starName nazwa gwiazdy
     * @param useGcvs czy połączyć się z GCVS w przypadku braku gwiazdy w bazie?
     */
    void StarManager::findStarByName(QString starName, bool useGcvs)
    {
        Star result = getStarFromDatabase(starName);
        if (result.isValid())
        {
            emit starFound(result, Database);
            return;
        }
        else if (useGcvs)
        {
            lookupStarInGcvs(starName);
        }
        else
        {
            emit starFound(result, Database);
            return;
        }

    }

    /**
     * Zgłasza żądanie wyszukania gwiazdy online w GCVS.
     *
     * Nie blokuje wykonania programu, więc żeby dostać się do informacji z
     * katalogu, należy podłączyć się pod sygnał starFound().
     *
     * @param starName nazwa gwiazdy
     */
    void StarManager::lookupStarInGcvs(QString starName)
    {
        if (m_netManager)
        {
            QString gcvsStarName = starName.toUpper().replace(" ", "+");
            QSettings settings;
            QUrl url(settings.value("network/gcvsUrl").toString().
                    arg(gcvsStarName));
            QNetworkReply* gcvsReply = m_netManager->get(QNetworkRequest(url));
            connect(gcvsReply, SIGNAL(finished()),
                    this, SLOT(gcvsReadFinished()));
        }
    }

    /**
     * Przetwarza odpowiedź z GCVS i emituje odpowiedni sygnał.
     */
    void StarManager::gcvsReadFinished()
    {
        Star result = Star::invalid();
        QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
        if (reply)
        {
            if (QNetworkReply::NoError == reply->error())
            {
                QString html = reply->readAll();

                if (isGcvsResponseValid(html))
                {
                    result = parseGcvsResponse(html);
                }
                else
                {
                    emit gcvsError(tr("Błędna odpowiedź GCVS:\n%1").arg(html));
                    return;
                }
            }
            else
            {
                emit gcvsError(tr("Błąd połączenia z GCVS:\n%1").
                        arg(reply->errorString()));
                return;
            }
        }

        // jeśli to co przyszło z GCVS jest poprawną gwiazdą, to zapisz do bazy
        // TODO: sprawdzać jakieś ustawienia, czy można automagicznie zapisywać
        if (result.isValid())
            addStar(result);

        emit starFound(result, Network);
    }

    /**
     * Sprawdza, czy odpowiedź serwera GCVS opisuje prawidłową gwiazdę.
     *
     * @param html kod HTML odpowiedzi (brzydki strasznie, swoją drogą :P )
     * @return true w przypadku odnalezienia jakiejś gwiazdy w odpowiedzi
     */
    bool StarManager::isGcvsResponseValid(const QString& html)
    {
        // dzielimy na linie
        QStringList lines = html.split("\n");
        if (lines.size() < 5)
            return false;
        // w piątej linii znajdują się interesujące dane, dzielimy na kolumny
        QStringList starInfo = lines[4].split("|");
        if (starInfo.size() < 10)
            return false;
        return true;
    }

    /**
     * Parsuje odpowiedź serwera GCVS, przetwarzając na obiekt gwiazdy.
     *
     * @param html kod HTML odpowiedzi
     * @return obiekt gwiazdy
     */
    Star StarManager::parseGcvsResponse(const QString& html)
    {
        QStringList starInfo = html.split("\n")[4].split("|");
        /**
         * Istotne części formatu tej linii:
         * [1] -> nazwa
         * [3] -> typ
         * [4] -> max. jasność
         * [5] -> min. jasność
         * [7] -> epoka bez początkowego 24...
         * [9] -> okres
         */
        QString starName = starInfo[1].replace("*", "").simplified();
        Star star(starName);
        star.setType(starInfo[3].simplified());
        star.setMagnitudeRange(starInfo[4].toFloat(), starInfo[5].toFloat());
        QString epoch = starInfo[7].simplified();
        if (!epoch.isEmpty())
        {
            star.setEpoch(2400000.0 + epoch.toDouble());
        }
        QString period = starInfo[9].simplified();
        if (!period.isEmpty())
        {
            star.setPeriod(period.toDouble());
        }
        return star;
    }

    /**
     * Zgłasza żądanie pobrania ostatnich obserwacji danej gwiazdy z AAVSO.
     *
     * Nie blokuje programu, po pobraniu danych zostaje wyemitowany sygnał
     * aavsoLookupFinished().
     *
     * @param starName nazwa gwiazdy
     */
    void StarManager::getAavsoObservations(QString starName)
    {
        if (m_netManager)
        {
            QString aavsoStarName = starName.replace(" ", "%20");
            QSettings settings;
            QUrl url(settings.value("network/aavsoQuickLookUrl").
                    toString().arg(aavsoStarName));
            QNetworkReply* aavsoReply = m_netManager->get(QNetworkRequest(url));
            connect(aavsoReply, SIGNAL(finished()),
                    this, SLOT(aavsoReadFinished()));
        }
    }

    /**
     * Przetwarza odpowiedź z AAVSO do postaci listy obserwacji.
     *
     * Emituje sygnał aavsoLookupFinished(), nawet dla pustej listy.
     */
    void StarManager::aavsoReadFinished()
    {
        QList<Observation*> observations;

        QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
        if (reply && QNetworkReply::NoError == reply->error())
        {
            QString html = reply->readAll();
            if (html.startsWith("Sorry"))  // :)
            {
                emit aavsoLookupFinished(observations);
                return;
            }

            QStringList tableRows = html.split("<TR>");
            QRegExp re(">(\\d{7}\\.\\d{1,6})<"              // JD pomiędzy znacznikami
                        "(?:.*)"                            // cokolwiek
                        "<a (?:.*)>(\\d+\\.\\d+)\\s*</a>"   // jasność wewnątrz linka
                        "(?:.*)"                            // cokolwiek
                        ">([a-zA-Z\\./- ]+)<");             // filtr
            re.setMinimal(true);
            foreach (QString row, tableRows)
            {
                // wynik wyrażenia znajduje się w tej linii
                if (re.indexIn(row) > -1)
                {
                    Dogz::Observation* obs = new Dogz::VisualObservation;
                    obs->setJd(re.cap(1).toDouble());
                    obs->setMagnitude(re.cap(2).toFloat());
                    obs->setNotes(re.cap(3));

                    observations.append(obs);
                }
            }
        }
        else
        {
            if (reply)
            {
                emit aavsoError(tr("Błąd połączenia z AAVSO:\n%1").
                        arg(reply->errorString()));
                return;
            }
        }

        emit aavsoLookupFinished(observations);
    }
}
